package org.phpgwt.phpservice;

import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.List;

import org.phpgwt.annotations.DateFormat;
import org.phpgwt.remote.PHPObject;
import org.phpgwt.remote.RemoteService;

import com.google.gwt.core.ext.Generator;
import com.google.gwt.core.ext.GeneratorContext;
import com.google.gwt.core.ext.TreeLogger;
import com.google.gwt.core.ext.UnableToCompleteException;
import com.google.gwt.core.ext.typeinfo.JClassType;
import com.google.gwt.core.ext.typeinfo.JMethod;
import com.google.gwt.core.ext.typeinfo.JParameter;
import com.google.gwt.core.ext.typeinfo.JParameterizedType;
import com.google.gwt.core.ext.typeinfo.NotFoundException;
import com.google.gwt.http.client.Request;
import com.google.gwt.http.client.RequestCallback;
import com.google.gwt.http.client.Response;
import com.google.gwt.i18n.client.DateTimeFormat;
import com.google.gwt.user.rebind.ClassSourceFileComposerFactory;
import com.google.gwt.user.rebind.SourceWriter;
import com.gwtent.serialization.client.json.JsonSerializer;

public class PHPRemoteServiceGenerator extends Generator {

	@Override
	public String generate(TreeLogger logger, GeneratorContext context, String typeName) throws UnableToCompleteException {
		try {
			JClassType classType = context.getTypeOracle().getType(typeName);
			final String asyncTypeName = typeName + "Async";
			JClassType asyncClassType = context.getTypeOracle().getType(asyncTypeName);

			String packageName = asyncClassType.getPackage().getName();
			String simpleName = asyncClassType.getSimpleSourceName() + "Generated";

			ClassSourceFileComposerFactory composer = new ClassSourceFileComposerFactory(packageName, simpleName);
			composer.addImplementedInterface(asyncClassType.getQualifiedSourceName());
			composer.addImport(JsonSerializer.class.getCanonicalName());
			composer.addImport(RemoteService.class.getCanonicalName());
			composer.addImport(RequestCallback.class.getCanonicalName());
			composer.addImport(Request.class.getCanonicalName());
			composer.addImport(Response.class.getCanonicalName());
			composer.addImport(ArrayList.class.getCanonicalName());
			composer.addImport(DateTimeFormat.class.getCanonicalName());

			PrintWriter printWriter = context.tryCreate(logger, packageName, simpleName);

			if (printWriter != null) {
				SourceWriter sw = composer.createSourceWriter(context, printWriter);

				for (JMethod method : asyncClassType.getMethods()) {
					sw.print(String.format("public void %s(", method.getName()));

					final JParameter[] parameters = method.getParameters();
					for (int i = 0; i < parameters.length; i++) {
						JParameter parameter = parameters[i];
						sw.print(String.format("final %s %s%s", parameter.getType().getParameterizedQualifiedSourceName(),
								parameter.getName(), i < parameters.length - 1 ? "," : ""));
					}
					sw.println("){");

					final JParameter callbackType = parameters[parameters.length - 1];
					String callbackName = callbackType.getName();
					final JClassType resultType = callbackType.getType().isParameterized().getTypeArgs()[0];
					String resultTypeName = resultType.getQualifiedSourceName();

					final JParameterizedType parametrizedType = resultType.isParameterized();
					String listTypeParameter = null;

					if (parametrizedType != null) {
						listTypeParameter = parametrizedType.getTypeArgs()[0].getQualifiedSourceName();
					}

					sw.println("  RequestCallback requestCallback = new RequestCallback() {");
					sw.println("    public void onResponseReceived(Request request, Response response) {");

					sw.println("      try{");
					if (List.class.getCanonicalName().equals(resultType.getQualifiedSourceName())) {
						sw.println(String.format("        %s result = new ArrayList<%s>();",
								resultType.getParameterizedQualifiedSourceName(), listTypeParameter));
						sw.println(String.format("        JsonSerializer.deserializeList(result, response.getText(), %s.class);",
								listTypeParameter));
					} else if (JsonSerializer.isSingle(resultType.getQualifiedSourceName())) {
						sw.println(String.format(
								"        %s result = (%s) JsonSerializer.deserializeSingle(response.getText(), %s.class);", resultTypeName,
								resultTypeName, resultTypeName));
					} else if (isPHPObject(Class.forName(resultType.getQualifiedSourceName()))) {
						sw.println(String.format("        %s result = JsonSerializer.deserializeObject(response.getText(), %s.class);",
								resultTypeName, resultTypeName));
					} else if (Void.class.getName().equals(resultType.getQualifiedSourceName())) {
						sw.println("        Void result = null;");
						sw.println("        if(!\"\".equals(response.getText()) && !\"null\".equals(response.getText())){");
						sw.println(String.format("          %s.onFailure(new Exception(response.getText()));", callbackName));
						sw.println("        }");
					} else {
						throw new Exception(String.format("Unsupported result type: %s.", resultTypeName));
					}
					sw.println(String.format("        %s.onSuccess(result);", callbackName));
					sw.println("      }catch(Exception e){");
					sw.println(String.format("        %s.onFailure(new Exception(e.getMessage()+\" \"+response.getText()));", callbackName));
					sw.println("      }");
					sw.println("    }");
					sw.println("");
					sw.println("    public void onError(Request request, Throwable exception) {");
					sw.println(String.format("      %s.onFailure(exception);", callbackName));
					sw.println("    }");
					sw.println("  };");
					sw.println();

					sw.print(String.format("  RemoteService.RPC(\"%s\", \"%s\", requestCallback", classType.getSimpleSourceName(),
							method.getName()));

					for (int i = 0; i < parameters.length - 1; i++) {
						final JParameter parameter = parameters[i];

						if ("Date".equals(parameter.getType().getSimpleSourceName())) {
							JMethod syncMethod = findMethod(classType.getMethods(), method.getName());

							if (syncMethod != null) {
								JParameter syncParameter = syncMethod.findParameter(parameter.getName());

								if (syncParameter.isAnnotationPresent(DateFormat.class)) {
									DateFormat dateFormat = syncParameter.getAnnotation(DateFormat.class);

									sw.print(String.format(", %s != null ? DateTimeFormat.getFormat(\"%s\").format(%s) : null",
											parameter.getName(), dateFormat.value(), parameter.getName()));
								} else {
									sw.print(String.format(", %s", parameter.getName()));
								}
							} else {
								throw new UnableToCompleteException();
							}
						} else {
							sw.print(String.format(", %s", parameter.getName()));
						}
					}

					sw.println(");");

					sw.println("}");
					sw.println();
				}

				sw.commit(logger);
			}

			return asyncTypeName + "Generated";
		} catch (Exception e) {
			e.printStackTrace();
			return null;
		}
	}

	private static JMethod findMethod(JMethod[] methods, String name) {
		for (JMethod method : methods) {
			if (method.getName().equals(name)) {
				return method;
			}
		}

		return null;
	}

	public static boolean isPHPObject(Class<?> clazz) {
		for (Class<?> iface : clazz.getInterfaces()) {
			if (PHPObject.class.equals(iface)) {
				return true;
			}
		}

		return false;
	}
}
